/*
According to apache license

This is fork of christocracy cordova-plugin-background-geolocation plugin
https://github.com/christocracy/cordova-plugin-background-geolocation

Differences to original version:

1. new methods isLocationEnabled, mMessageReciever, handleMessage
*/

package com.tenforwardconsulting.cordova;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieManager;
import android.location.LocationManager;
import android.Manifest;
import android.content.pm.PackageManager;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.marianhello.bgloc.Config;
import com.marianhello.bgloc.HttpPostService;
import com.marianhello.bgloc.LocationService;
import com.marianhello.cordova.PermissionHelper;
import com.marianhello.bgloc.data.LocationDAO;
import com.marianhello.bgloc.data.ConfigurationDAO;
import com.marianhello.bgloc.data.DAOFactory;
import com.marianhello.bgloc.data.BackgroundLocation;

import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BackgroundGeolocationPlugin extends CordovaPlugin {
  private static final String TAG = "BGPlugin";

  public static String ACTION_CURRENT = "";

  public static final String ACTION_START = "start";
  public static final String ACTION_STOP = "stop";
  public static final String ACTION_CONFIGURE = "configure";
  public static final String ACTION_START_TRIP = "startTrip";
  public static final String ACTION_STOP_TRIP = "stopTrip";
  public static final String ACTION_TRANSFER_LOCATIONS = "transferLocations";
  public static final String ACTION_STOP_TRANSFER_LOCATIONS = "stopTransferLocations";
  public static final String ACTION_REGISTER_STATUS_UPDATE = "registerStatusUpdate";
  public static final String ACTION_LOCATION_ENABLED_CHECK = "isLocationEnabled";
  public static final String ACTION_SHOW_LOCATION_SETTINGS = "showLocationSettings";
  public static final String REGISTER_MODE_CHANGED_RECEIVER = "watchLocationMode";
  public static final String UNREGISTER_MODE_CHANGED_RECEIVER = "stopWatchingLocationMode";
  public static final String ACTION_GET_ALL_LOCATIONS = "getLocations";
  public static final String ACTION_DELETE_LOCATION = "deleteLocation";
  public static final String ACTION_DELETE_ALL_LOCATIONS = "deleteAllLocations";
  public static final String ACTION_GET_CONFIG = "getConfig";
  public static final String ACTION_GET_STATUS = "getStatus";
  public static final String ACTION_AUTO_OPEN = "autoOpen";
  public static final String ACTION_AUTO_CLOSE = "autoClose";

  public static final int START_REQ_CODE = 0;
  public static final int PERMISSION_DENIED_ERROR = 20;

  public static int TRIP_ID = 0;
  public static final String[] permissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};

  /**
   * Messenger for communicating with the service.
   */
  private Messenger mService = null;
  /**
   * Flag indicating whether we have called bind on the service.
   */
  private Boolean mIsBound = false;

  private Boolean isServiceRunning = false;
  private Boolean isLocationModeChangeReceiverRegistered = false;

  private LocationDAO dao;
  private Config config;
  private CallbackContext callbackContext;
  private CallbackContext statusUpdateCallbackContext;
  private CallbackContext actionStartServiceCallbackContext;
  private CallbackContext actionStartTripCallbackContext;
  private CallbackContext actionStopTripCallbackContext;
  private CallbackContext locationModeChangeCallbackContext;
  private CallbackContext actionGetStatusCallbackContext;
  private ExecutorService executorService;


  /**
   * Handler of incoming messages from service.
   */
  class IncomingHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
        case LocationService.MSG_START_TRIP_RESPONSE:
          Log.d(TAG, "Sending start trip response...");
          PluginResult startTripResult = null;
          String strStartTripResult = msg.getData().getString("msg");
          if (strStartTripResult == null) {
            startTripResult = new PluginResult(PluginResult.Status.ERROR, msg.arg1);
          } else {
            try {
              TRIP_ID = new JSONObject(strStartTripResult).getInt("tripId");
            } catch (JSONException e) {
              e.printStackTrace();
            }
            startTripResult = new PluginResult(PluginResult.Status.OK, strStartTripResult);
          }
          startTripResult.setKeepCallback(true);
          actionStartTripCallbackContext.sendPluginResult(startTripResult);
          break;
        case LocationService.MSG_STOP_TRIP_RESPONSE:
          Log.d(TAG, "Sending stop trip response...");
          PluginResult stopTripResult = null;
          String strStopTripResult = msg.getData().getString("msg");
          if (strStopTripResult == null) {
            stopTripResult = new PluginResult(PluginResult.Status.ERROR, msg.arg1);
          } else {
            stopTripResult = new PluginResult(PluginResult.Status.OK, strStopTripResult);
          }
          stopTripResult.setKeepCallback(true);
          actionStopTripCallbackContext.sendPluginResult(stopTripResult);
          break;
        case LocationService.MSG_STATUS:
          Log.d(TAG, "Sending trip status response...");
          PluginResult tripStatusResult = null;
          String strTripStatusResult = msg.getData().getString("msg");
          if (strTripStatusResult == null) {
            tripStatusResult = new PluginResult(PluginResult.Status.ERROR, msg.arg1);
          } else {
            tripStatusResult = new PluginResult(PluginResult.Status.OK, strTripStatusResult);
          }
          tripStatusResult.setKeepCallback(true);
          actionGetStatusCallbackContext.sendPluginResult(tripStatusResult);
          break;
        case LocationService.MSG_STATUS_UPDATE:
          Log.d(TAG, "Sending transfer status update...");
          // Remote-checkpoint
//                	if (config.isDebugging()) {
//                    	try {
//            	        	JSONObject json = new JSONObject();
//            	        	json.put("createdBy", config.getHttpHeaders().get("user-name"));
//            	        	json.put("content", msg.getData().getString("msg"));
//            	        	json.put("checkpointName", "BeforeSendUpdateMsg");
//            	        	json.put("type", msg.getData().getString("msg") == null ? "error" : "info");
//            	        	postRemoteLog(json);
//                    	} catch (JSONException e) {
//                            Log.e(TAG, e.toString());
//            		    }
//                    }
          PluginResult statusUpdateResult = new PluginResult(PluginResult.Status.OK, msg.getData().getString("msg"));
          statusUpdateResult.setKeepCallback(true);
          callbackContext.sendPluginResult(statusUpdateResult);
          break;
        case LocationService.MSG_GPS_STATUS_UPDATE:
          Log.d(TAG, "Sending GPS status update...");
          PluginResult gpsStatusUpdateResult = new PluginResult(PluginResult.Status.OK, msg.arg1);
          gpsStatusUpdateResult.setKeepCallback(true);
          statusUpdateCallbackContext.sendPluginResult(gpsStatusUpdateResult);
          break;
                 /*case LocationService.MSG_NETWORK_STATUS_UPDATE:
                  Log.d(TAG, "Sending network status update...");
                	PluginResult networkStatusUpdateResult = new PluginResult(PluginResult.Status.ERROR, msg.arg1);
                	networkStatusUpdateResult.setKeepCallback(true);
                	statusUpdateCallbackContext.sendPluginResult(networkStatusUpdateResult);
                	break;*/
        default:
          super.handleMessage(msg);
      }
    }
  }

  final Messenger mMessenger = new Messenger(new IncomingHandler());

  /**
   * Class for interacting with the main interface of the service.
   */
  private ServiceConnection mConnection = new ServiceConnection() {
    public void onServiceConnected(ComponentName className, IBinder service) {
      // This is called when the connection with the service has been
      // established, giving us the object we can use to
      // interact with the service.  We are communicating with the
      // service using a Messenger, so here we get a client-side
      // representation of that from the raw IBinder object.
      mService = new Messenger(service);
      mIsBound = true;

      // We want to monitor the service for as long as we are
      // connected to it.
      try {
        Message msg = Message.obtain(null,
          LocationService.MSG_REGISTER_CLIENT);
        msg.replyTo = mMessenger;
        mService.send(msg);
      } catch (RemoteException e) {
        // In this case the service has crashed before we could even
        // do anything with it; we can count on soon being
        // disconnected (and then reconnected if it can be restarted)
        // so there is no need to do anything here.
      }
    }

    public void onServiceDisconnected(ComponentName className) {
      // This is called when the connection with the service has been
      // unexpectedly disconnected -- that is, its process crashed.
      mService = null;
      mIsBound = false;
    }
  };

  private BroadcastReceiver locationModeChangeReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      Log.d(TAG, "Received MODE_CHANGED_ACTION action");
      if (locationModeChangeCallbackContext != null) {
        PluginResult result;
        try {
          int isLocationEnabled = BackgroundGeolocationPlugin.isLocationEnabled(context) ? 1 : 0;
          result = new PluginResult(PluginResult.Status.OK, isLocationEnabled);
          result.setKeepCallback(true);
        } catch (SettingNotFoundException e) {
          result = new PluginResult(PluginResult.Status.ERROR, "Location setting error occured");
        }
        locationModeChangeCallbackContext.sendPluginResult(result);
      }
    }
  };

  public boolean execute(String action, final JSONArray data, final CallbackContext callbackContext) {
    Activity activity = this.cordova.getActivity();
    Context context = activity.getApplicationContext();
    Log.i(TAG, "execute -> " + action + "-> " + data.toString());
    try {
      if (ACTION_START.equals(action)) {
        actionStartServiceCallbackContext = callbackContext;
        if (config == null) {
          callbackContext.error("Plugin not configured. Please call configure method first.");
          return true;
        }

        if (hasPermissions()) {
          startAndBindBackgroundService();
          // Log.i(TAG, "bg service started.");

        } else {
          PermissionHelper.requestPermissions(this, START_REQ_CODE, permissions);
        }
        ACTION_CURRENT = action;
        callbackContext.success();
        Log.i(TAG, ACTION_START + " " + action);
        return true;
      } else if (ACTION_STOP.equals(action)) {
        doUnbindService();
        stopBackgroundService();
        callbackContext.success();
        Log.i(TAG, ACTION_STOP + " " + action);
        ACTION_CURRENT = action;
        return true;
      } else if (ACTION_CONFIGURE.equals(action)) {
        try {
          this.callbackContext = callbackContext;
          config = Config.fromJSONArray(data);
          //prepareHeaders(config);
          persistConfiguration(config);
          // Log.i(TAG, "bg service configured: " + config.toString());
          callbackContext.success(); //we cannot do this
        } catch (JSONException e) {
          callbackContext.error("Configuration error: " + e.getMessage());
        } catch (NullPointerException e) {
          callbackContext.error("Configuration error: " + e.getMessage());
        }
        Log.i(TAG, ACTION_CONFIGURE + " " + action);
        return true;
      } else if (ACTION_REGISTER_STATUS_UPDATE.equals(action)) {
        try {
          this.statusUpdateCallbackContext = callbackContext;

        } catch (Exception e) {
          Log.e(TAG, "bg service register status update exception: " + e.getMessage());
        }

        return true;
      } else if (ACTION_START_TRIP.equals(action)) {
        executorService.execute(new Runnable() {
          public void run() {
            actionStartTripCallbackContext = callbackContext;
            try {
              String strStartBundle = data.getString(0);
              // Log.i(TAG, "bg service start trip: " + strStartBundle);
              Message msg = Message.obtain(null, LocationService.MSG_START_TRIP);
              Bundle bundle = new Bundle();
              bundle.putString("msg", strStartBundle);
              msg.replyTo = mMessenger;
              msg.setData(bundle);
              if (mService != null)
                mService.send(msg);

            } catch (JSONException e) {
              callbackContext.error("Start trip error: " + e.getMessage());
            } catch (NullPointerException e) {
              callbackContext.error("Start trip error: " + e.getMessage());
            } catch (RemoteException e) {
              callbackContext.error("Start trip error: " + e.getMessage());
            }
          }
        });
        Log.i(TAG, ACTION_START_TRIP + " " + action);
        return true;
      } else if (ACTION_STOP_TRIP.equals(action)) {
        executorService.execute(new Runnable() {
          public void run() {
            actionStopTripCallbackContext = callbackContext;
            try {
              String strStopBundle = data.getString(0);
              Log.d(TAG, "bg service stop trip. ");
              Message msg = Message.obtain(null, LocationService.MSG_STOP_TRIP);
              Bundle bundle = new Bundle();
              bundle.putString("msg", strStopBundle);
              msg.replyTo = mMessenger;
              msg.setData(bundle);
              if (mService != null)
                mService.send(msg);
            } catch (RemoteException e) {
              callbackContext.error("Stop trip error: " + e.getMessage());
            } catch (JSONException e) {
              e.printStackTrace();
            }
          }
        });

        return true;
      } else if (ACTION_TRANSFER_LOCATIONS.equals(action)) {
        executorService.execute(new Runnable() {
          public void run() {
            try {
              Log.d(TAG, "bg service transfer location. ");
              Message msg = Message.obtain(null, LocationService.MSG_TRANSFER_LOCATIONS);
              Bundle bundle = new Bundle();
              bundle.putInt("tripId", BackgroundGeolocationPlugin.TRIP_ID);
              msg.replyTo = mMessenger;
              msg.setData(bundle);
              if (mService != null)
                mService.send(msg);
            } catch (RemoteException e) {
              callbackContext.error("Transfer location error: " + e.getMessage());
            }
          }
        });
        callbackContext.success();
        return true;
      } else if (ACTION_STOP_TRANSFER_LOCATIONS.equals(action)) {
        executorService.execute(new Runnable() {
          public void run() {
            try {
              Log.d(TAG, "bg service stop transfer location. ");
              Message msg = Message.obtain(null, LocationService.MSG_STOP_TRANSFER);
              msg.replyTo = mMessenger;
              if (mService != null)
                mService.send(msg);
            } catch (RemoteException e) {
              callbackContext.error("Stop transfer error: " + e.getMessage());
            }
          }
        });
        callbackContext.success();
        return true;
      } else if (ACTION_LOCATION_ENABLED_CHECK.equals(action)) {
        Log.d(TAG, "location services enabled check");
        try {
          int isLocationEnabled = BackgroundGeolocationPlugin.isLocationEnabled(context) ? 1 : 0;
          callbackContext.success(isLocationEnabled);
        } catch (SettingNotFoundException e) {
          callbackContext.error("Location setting error occured");
        }

        return true;
      } else if (ACTION_SHOW_LOCATION_SETTINGS.equals(action)) {
        showLocationSettings();
        // TODO: call success/fail callback

        return true;
      } else if (REGISTER_MODE_CHANGED_RECEIVER.equals(action)) {
        this.locationModeChangeCallbackContext = callbackContext;
        registerLocationModeChangeReceiver();
        // TODO: call success/fail callback

        return true;
      } else if (UNREGISTER_MODE_CHANGED_RECEIVER.equals(action)) {
        unregisterLocationModeChangeReceiver();
        this.locationModeChangeCallbackContext = null;
        // TODO: call success/fail callback

        return true;
      } else if (ACTION_GET_ALL_LOCATIONS.equals(action)) {
        try {
          callbackContext.success(getAllLocations());
        } catch (JSONException e) {
          callbackContext.error("Converting locations to JSON failed.");
        }

        return true;
      } else if (ACTION_DELETE_LOCATION.equals(action)) {
        try {
          deleteLocation(data.getLong(0));
          callbackContext.success();
        } catch (JSONException e) {
          callbackContext.error("Configuration error: " + e.getMessage());
        }

        return true;
      } else if (ACTION_DELETE_ALL_LOCATIONS.equals(action)) {
        deleteAllLocations();
        callbackContext.success();

        return true;
      } else if (ACTION_GET_CONFIG.equals(action)) {
        try {
          callbackContext.success(retrieveConfiguration());
        } catch (JSONException e) {
          callbackContext.error("Configuration error: " + e.getMessage());
        }
        return true;
      } else if (ACTION_GET_STATUS.equals(action)) {
        this.actionGetStatusCallbackContext = callbackContext;
        try {
          Message msg = Message.obtain(null, LocationService.MSG_STATUS);
          msg.replyTo = mMessenger;
          if (mService != null)
            mService.send(msg);
        } catch (RemoteException e) {
          actionGetStatusCallbackContext.error("getStatus error: " + e.getMessage());
        }
        return true;
      } else if (ACTION_AUTO_OPEN.equals(action)) {
        try {
          Log.d(TAG, "bg service auto open. ");
          Message msg = Message.obtain(null, LocationService.MSG_AUTO_START_STOP_OPEN);
          msg.replyTo = mMessenger;
          if (mService != null)
            mService.send(msg);
          callbackContext.success();
        } catch (RemoteException e) {
          callbackContext.error("auto open error: " + e.getMessage());
        }
        return true;
      } else if (ACTION_AUTO_CLOSE.equals(action)) {
        try {
          Log.d(TAG, "bg service auto close. ");
          Message msg = Message.obtain(null, LocationService.MSG_AUTO_START_STOP_CLOSE);
          msg.replyTo = mMessenger;
          if (mService != null)
            mService.send(msg);
          callbackContext.success();
        } catch (RemoteException e) {
          callbackContext.error("auto close error: " + e.getMessage());
        }
        return true;
      }
    }catch (Exception e){

    }
    return false;
  }


  @Override
  protected void pluginInitialize() {
    Log.d(TAG, "initializing plugin");
    super.pluginInitialize();

    dao = DAOFactory.createLocationDAO(getContext());
    executorService = Executors.newSingleThreadExecutor();
  }

  /**
   * Override method in CordovaPlugin.
   * Checks to see if it should turn off
   */
  @Override
  public void onDestroy() {
    Log.d(TAG, "destroying plugin");
    unregisterLocationModeChangeReceiver();
    // Unbind from the service
    doUnbindService();
    if (config.getStopOnTerminate()) {
      stopBackgroundService();
    }
    super.onDestroy();
  }

  public Context getContext() {
    return this.cordova.getActivity().getApplicationContext();
  }

  protected void startAndBindBackgroundService() {
    startBackgroundService();
    doBindService();
  }

  protected void startBackgroundService() {
    if (!isServiceRunning) {
      Activity activity = this.cordova.getActivity();
      Intent locationServiceIntent = new Intent(activity, LocationService.class);
      locationServiceIntent.putExtra("config", config);
      locationServiceIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
      // start service to keep service running even if no clients are bound to it
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        activity.startForegroundService(locationServiceIntent);
      } else {
        activity.startService(locationServiceIntent);
      }
      isServiceRunning = true;
    }
  }

  protected void stopBackgroundService() {
    if (isServiceRunning) {
      // Log.i(TAG, "Stopping bg service");
      Activity activity = this.cordova.getActivity();
      activity.stopService(new Intent(activity, LocationService.class));
      isServiceRunning = false;
    }
  }

  protected void prepareHeaders(Config config) throws JSONException {
    if (config != null) {
      CookieManager cm = CookieManager.getInstance();
      String strCookies = cm.getCookie(config.getUrl());
      config.getHttpHeaders().put("Cookie", strCookies);
      config.getHttpHeaders().put("Accept-Language", "zh-CN,en-US;q=0.8");
//            config.getHttpHeaders().put("Accept-Encoding", "gzip, deflate");
    }
  }

  void doBindService() {
    // Establish a connection with the service.  We use an explicit
    // class name because there is no reason to be able to let other
    // applications replace our component.
    if (!mIsBound) {
      Activity activity = this.cordova.getActivity();
      Intent locationServiceIntent = new Intent(activity, LocationService.class);
      locationServiceIntent.putExtra("config", config);
      activity.bindService(locationServiceIntent, mConnection, Context.BIND_IMPORTANT);
    }
  }

  void doUnbindService() {
    if (mIsBound) {
      // If we have received the service, and hence registered with
      // it, then now is the time to unregister.
      if (mService != null) {
        try {
          Message msg = Message.obtain(null,
            LocationService.MSG_UNREGISTER_CLIENT);
          msg.replyTo = mMessenger;
          mService.send(msg);
        } catch (RemoteException e) {
          // There is nothing special we need to do if the service
          // has crashed.
        }

        // Detach our existing connection.
        Activity activity = this.cordova.getActivity();
        activity.unbindService(mConnection);
        mIsBound = false;
      }
    }
  }

  @TargetApi(Build.VERSION_CODES.KITKAT)
  public Intent registerLocationModeChangeReceiver() {
    if (isLocationModeChangeReceiverRegistered) {
      return null;
    }

    isLocationModeChangeReceiverRegistered = true;
    return getContext().registerReceiver(locationModeChangeReceiver, new IntentFilter(LocationManager.MODE_CHANGED_ACTION));
  }

  public void unregisterLocationModeChangeReceiver() {
    if (!isLocationModeChangeReceiverRegistered) {
      return;
    }

    getContext().unregisterReceiver(locationModeChangeReceiver);
    isLocationModeChangeReceiverRegistered = false;
  }

  public void showLocationSettings() {
    Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    cordova.getActivity().startActivity(settingsIntent);
  }

  public static boolean isLocationEnabled(Context context) throws SettingNotFoundException {
    int locationMode = 0;
    String locationProviders;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      locationMode = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE);
      return locationMode != Settings.Secure.LOCATION_MODE_OFF;

    } else {
      locationProviders = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
      return !TextUtils.isEmpty(locationProviders);
    }
  }

  public JSONArray getAllLocations() throws JSONException {
    JSONArray jsonLocationsArray = new JSONArray();
    Collection<BackgroundLocation> locations = dao.getAllLocations();
    for (BackgroundLocation location : locations) {
      jsonLocationsArray.put(location.toJSONObject());
    }
    return jsonLocationsArray;
  }

  public void deleteLocation(Long locationId) {
    dao.deleteLocation(locationId);
  }

  public void deleteAllLocations() {
    dao.deleteAllLocations();
  }

  public void persistConfiguration(Config config) throws NullPointerException {
    Context context = this.cordova.getActivity().getApplicationContext();
    ConfigurationDAO dao = DAOFactory.createConfigurationDAO(context);

    dao.persistConfiguration(config);
  }

  public JSONObject retrieveConfiguration() throws JSONException {
    Context context = this.cordova.getActivity().getApplicationContext();
    ConfigurationDAO dao = DAOFactory.createConfigurationDAO(context);
    Config config = dao.retrieveConfiguration();
    if (config != null) {
      return config.toJSONObject();
    }
    return null;
  }

  public boolean hasPermissions() {
    for (String p : permissions) {
      if (!PermissionHelper.hasPermission(this, p)) {
        return false;
      }
    }
    return true;
  }

  public void onRequestPermissionResult(int requestCode, String[] permissions,
                                        int[] grantResults) throws JSONException {
    for (int r : grantResults) {
      if (r == PackageManager.PERMISSION_DENIED) {
        Log.d(TAG, "Permission Denied!");
        actionStartServiceCallbackContext.error(PERMISSION_DENIED_ERROR);
        actionStartServiceCallbackContext = null;
        return;
      }
    }
    switch (requestCode) {
      case START_REQ_CODE:
        startAndBindBackgroundService();
        break;
    }
  }

  public String postRemoteLog(Object jsonObj) {
    String response = null;
    try {
      response = HttpPostService.postRemoteLog(config.getHttpHeaders().get("remote").toString(), jsonObj, null);

    } catch (IOException e) {
      Log.e(TAG, e.toString());
    }

    return response;
  }
}
